/**
 * Analyser Scala Code.  Copyright 2008, Three Rings Design inc.
 * Author: Robin Barooah
 * Released under a BSD license.
 *
 * see: http://code.google.com/p/as3-dependency-tracer/
 */
import scala.xml._
import scala.collection.mutable.{Set, HashSet, HashMap}

package com.threerings.tools.flex.dependency {
    
    abstract class DependencyAnalyser {

        def tracing :Boolean

        def trace (message:String) {
            if (tracing) println(message)
        }

        /**
         * Trait for breaking down XML
         */ 
        trait XMLFields {
            def xml:NodeSeq

            //todo: should this cause some sort of error if there is more than one node?
            protected def node (name:String) = (xml \ name)
            protected def nodes (name:String) = (xml \ name)
            protected def string (name:String) = node(name) text
        }
    
        trait Node {
            def id () :String
        }
        
        class Report (val xml:NodeSeq) extends XMLFields {
            lazy val scripts = new Scripts(this, nodes("scripts"))
            lazy val externals = new Externals(this, nodes("external-defs"))
        
            /** find scripts matching the given name */
            def find (name:String) = scripts.find(name)
        }
            
        class Scripts (val report:Report, val xml:NodeSeq) extends XMLFields {
            lazy val scripts = nodes("script") map (new Script(this, _))
            def find (name:String) = scripts filter (_.id.equals(name))
        }

        class Script (val scripts:Scripts, val xml:NodeSeq) extends XMLFields with PathNode {
            lazy val name = string ("@name")
            lazy val defs = new Definition(this, node("def"))
            lazy val deps = nodes("dep") map (new Dependency(this, _))
            lazy val prereqs = nodes("pre") map (new Prerequisite(this, _))
            lazy val id = defs.id
        
            /**
             * Scripts can be the starting point for an analysis.
             */
            def findDependencies (analyser:Analyser) {
                findDependencies(Origin to this, analyser)
            }
        
            def followPaths (path:Path, analyser:Analyser) = {
                trace("following paths from "+this)
                if (!(analyser.immune(this))) {
                    for (d <- deps) d.findDependencies(path, analyser)
                    for (p <- prereqs) p.findDependencies(path, analyser)
                }
            }
                
            override def toString () :String = "script defining "+defs
            
            override def definition () = id
        }
    
        class BasicNode(val xml:NodeSeq) extends XMLFields {
            lazy val id = string("@id")     
        }
    
        class Definition(val script:Script, override val xml:NodeSeq) extends BasicNode(xml) {              
            override def toString () :String = "class "+id
        }

        class Link(override val xml:NodeSeq) extends BasicNode(xml) {
            def findDependencies (path:Path, analyser:Analyser) {
                trace("following "+this)
                val scripts = analyser.report find id
                if (scripts.size < 1) findExternal(path, analyser)
                else if (scripts.size == 1) for (s <- scripts) s.findDependencies(path.to(s), analyser)
                else throw new Fail("ambiguous script id "+id)
            }
        
            def findExternal(path:Path, analyser:Analyser) {
                trace("finding external "+id)
                val externals = analyser.report.externals find id
                if (externals.size < 1) throw new Fail("missing script/external id "+id)
                if (externals.size == 1) for (e <- externals) e.findDependencies(path.to(e), analyser)
                else throw new Fail("ambiguous external id "+id)
            }
        }

        class Prerequisite(val script:Script, override val xml:NodeSeq) extends Link(xml) {
            override def toString () = script.id + " prerequisite "+id
        }
    
        class Dependency(val script:Script, override val xml:NodeSeq) extends Link(xml) {
            override def toString () = script.id + " dependency "+id
        }
        
        class Externals(val report:Report, override val xml:NodeSeq) extends XMLFields {
            lazy val externals = nodes("ext") map (new External(this,_))
        
            def find (id:String) = externals filter (_.id.equals(id))
        }
    
        class External(val externals:Externals, override val xml:NodeSeq) 
            extends BasicNode(xml) with PathNode    
        {               
            override def followPaths(prior:Path, analyser:Analyser) = {
                analyser.category(this).complete(analyser, prior)
            }
                
            override def toString () = "externally defined class "+id
            
            override def definition = id
        }
    
        abstract class Categorized {
            def complete (analyser:Analyser, path:Path)
        }
    
        case class Available(external:External) extends Categorized {
            def complete (analyser:Analyser, path:Path) = {
                trace(external + " is available")
            }
        }
    
        case class Unavailable(external:External) extends Categorized {
            def complete (analyser:Analyser, path:Path) {
                trace(external+" is unavailable")
                analyser.unavailable += path
            }
        }
    
        case class Unknown(external:External) extends Categorized {
            def complete (analyser:Analyser, path:Path) {
                trace(external+" is unknown")
                analyser.unknown += path
            }
        }
    
        class Inclusion (pattern:String) {
            import scala.util.matching.Regex        
            lazy val regex = pattern.r
        
            def matches (e:PathNode) :Boolean = (regex findFirstIn e.id) match {
                case None => false
                case _ => true
            }
        }
    
        case class Package (val name:String) extends Inclusion("^"+name+"*")
        case class Class (val name:String) extends Inclusion("^"+name)
        
        /**
         * This object contains the list of what's ok to use on the server.
         */
        class MatchFilter (included:List[Inclusion]) extends Filter {       
            def includes (node:PathNode) :Boolean = includes(node, included)
        
            def includes (node:PathNode, list:List[Inclusion]) :Boolean = list match {
                case List() => false
                case inclusion :: remaining => 
                    if (inclusion.matches(node)) true
                    else includes (node, remaining) 
            }       
        }
            
        object Immune extends MatchFilter (
            List(
                Package ("com.whirled.game")
            )
        )       
    
        object NoMatches extends Filter {
            def includes (node:PathNode) :Boolean = false
        }
    
        trait Filter {
            def includes (node:PathNode) :Boolean
        }
    
        class Categorizer(externals:Externals, inclusions:Filter, exclusions:Filter, fake:Filter) {     
            lazy val categorized = new HashMap[String, Categorized]     

            for (e <- externals.externals) categorized += (e.id -> categorize(e))

            if (tracing) dump

            def category (e:External) = (categorized get e.id) match {
                case Some(category) => category
                case None => throw Fail("found uncategorized "+e)
            }

            private def categorize (e:External) = {
                if (fake includes e) Unavailable(e)
                else if (inclusions includes e) Available(e)
                else if (exclusions includes e) Unavailable(e)
                else Unknown(e)
            }
            
            def dump {
                categorized.values foreach (println(_))
            }
        }
    
        def config:Configuration
        
        trait Configuration {
            def startingClass:String
            def showUnknown:Boolean
        }
            
        case class Fail(message:String) extends Exception(message)
    
        case class Vertex(val from:Node, val to:Node)
        {
            val value = from.id+"->"+to.id

            override def hashCode = value.hashCode
        
            override def equals (other:Any) = other match {
                case v:Vertex => v.value.equals(value)
                case _ => false
            }
                
            override def toString () = "vertex from "+from+" to "+to
        }
    
        trait Path {
            /**
             * Produce a path pointing back to this one from the one provided.
             */
            def to (node:PathNode) :Path
            def vertex:Vertex
            override def toString () = to.definition + prior.next
            def next:String = " -> " + to.definition + prior.next
            def prior:Path
            def to:PathNode
        }
    
        def prettyPrintPath (path:Path) {           
            def printIndented(path:Path) {
                if (path != Origin) {
                    println ("  -> "+path.to.definition)
                    printIndented(path.prior)
                }
            }
            
            println (path.to.definition)
            printIndented(path.prior)           
        }
    
        trait PathNode extends Node {
            def followPaths(path:Path, analyser:Analyser)
        
            def definition:String
        
            /**
             * Follow available paths from this node as long as the one used to get here hasn't
             * already been traversed.
             */
            def findDependencies (path:Path, analyser:Analyser) =
            {
                if (!analyser.breadcrumbs.contains(path.vertex)) {
                    analyser.breadcrumbs += path.vertex         
                    followPaths(path, analyser)         
                }
            }
        }
    
        object Origin extends PathNode with Path {
            def to (node:PathNode) :Path = new SimplePath(this, this, node)
            lazy val vertex = new Vertex(this, this)
            def id = "-origin-"
            def followPaths(prior:Path, analyser:Analyser) = {}
            override def toString () = "Origin"
            def definition = toString ()            
            def to :PathNode = this
            def prior :Path = this
            override def next = ""
        }
    
        class SimplePath(val prior:Path, val from:PathNode, val to:PathNode) extends Path {
            def to (node:PathNode) = new SimplePath(this, to, node)
            lazy val vertex = new Vertex(from, to)
        }
    
        trait Analyser {
            def report:Report
            def category (e:External) :Categorized
            def breadcrumbs:Set[Vertex]
            def unavailable:Set[Path]
            def unknown:Set[Path]       
            def immune (node:PathNode) :Boolean
        }

        abstract class Analysis(filename:String) extends Analyser {     
            val xml = XML.loadFile(filename)
            val report = new Report(xml)
            def categorizer:Categorizer
            val unavailable = new HashSet[Path]
            val unknown = new HashSet[Path]
            val breadcrumbs = new HashSet[Vertex]
        
            def immune (node:PathNode) = {
                trace ("checking immunity of "+node)
                Immune.includes(node)
            }
        
            def category (e:External) = categorizer category e
        
            /**
             * Output the report data.
             */
            def output {
                def outputSet(set:Set[Path], name:String) = if (! set.isEmpty) {
                    println(name + " classes referenced from "+config.startingClass+"\n")
                    for (path <- set) {
                        prettyPrintPath(path)
                        println()
                    }
                }
                
                outputSet(unavailable, "Unavailable")
                if (config.showUnknown) outputSet(unknown, "Unknown")
            }
        
            /**
             * Run the analysis.
             */
            def run {           
                val start = (report find config.startingClass)
                if (start.size < 1) throw Fail("can't find the starting class "+config.startingClass)
                else if (start.size > 1) throw Fail("ambigous starting class "+config.startingClass)
                for (s <- start) s.findDependencies(this)
                output
            } 
        }
        
        /**
         * Static values associated with swf files.
         */
        object SwcFile {
            val catalogName = "catalog.xml"
            val libaryName = "library.swf"
        }
        
        /**
         * A representation of a swc file with tool for decomposing its catalog.
         */
        case class SwcFile(name:String) {   
            import java.io._
            import java.util.zip._
            import SWCCatalogFormat._       

            /**
             * A version of the catalog with a filter containing defined symbols.
             */
            class FilterCatalog (override val xml:NodeSeq) extends Catalog(xml) {
                class DefinitionFilter(catalog:Catalog) extends Filter {
                    val set = new HashSet[String]
                    for (l <- catalog.libraries.libraries; s <- l.scripts; d <- s.defs) set += d.id
                    def includes (node:PathNode) = set.contains(node.id)
                    def dump = for (id <- set) println(id)                  
                }
                
                lazy val definitions = new DefinitionFilter(catalog)
            }
            
            /**
             * Search the zip input stream for a named entry.
             */
            private def search (name:String, z:ZipInputStream) : Option[ZipEntry] = {
                val entry = z.getNextEntry()
                if (entry == null) None
                else if (name.equals(entry.getName())) Some(entry)
                else search(name, z)
            }
            
            /**
             * Return a catalog object generated from the swf file.
             */
            lazy val catalog:FilterCatalog = 
                try {
                    val fileInput = new FileInputStream(name)
                    val zipInput = new ZipInputStream(fileInput)
                    search (SwcFile.catalogName, zipInput) match {
                        case None => throw Fail("couldn't find "+SwcFile.catalogName+" in "+name)
                        case Some(e) => new FilterCatalog(XML.load(zipInput))
                    }
                } catch {
                    case e:FileNotFoundException => throw Fail("couldn't find swc file "+name)
                }
        }
        
        case class FilterTracer(val filter:Filter, val name:String) extends Filter
        {
            def includes (node:PathNode) :Boolean = {
                val result = filter.includes(node)
                trace((if (!result) "not-" else "") + name + "(" + node + ")")
                result
            }
        }
        
        /**
         * Combine a list of filters together into a single filter.
         */
        case class FilterGroup(val filters:List[Filter]) extends Filter {
            
            def includes (list:List[Filter], node:PathNode) :Boolean = list match {
                case List() => false
                case filter :: remaining =>
                    if (filter.includes(node)) true
                    else includes (remaining, node)
            }
            
            def includes (node:PathNode) :Boolean = includes (filters, node)
        }
        
        /**
         * Convert the SWC Catalog XML format into objects
         */
        object SWCCatalogFormat {

            class Catalog (val xml:NodeSeq) extends XMLFields {
                lazy val libraries = new Libraries(this, nodes("libraries"))            
            }

            class Libraries (val catalog:Catalog, val xml:NodeSeq) extends XMLFields {
                lazy val libraries = nodes("library") map (new Library(this, _))            
            }

            class Library (val libraries:Libraries, val xml:NodeSeq) extends XMLFields {
                lazy val path = string("@path")
                lazy val scripts = nodes("script") map (new Script(this, _))
            }

            class Script (val library:Library, val xml:NodeSeq) extends XMLFields {
                lazy val name = string("@name")
                lazy val defs = nodes("def") map (new Def(this, _))
            }

            class Def (val script:Script, val xml:NodeSeq) extends XMLFields {
                lazy val id = string("@id")
            }
        }
    }
    
    abstract class ToolSet extends DependencyAnalyser {
        def tracing = false;
    }


    /**
     * Define a simple interface to drive the analyser from an ant task.
     */
    import java.util.{List => JList, Iterator => JIterator};
    class AntAnalyser(javailable:JList[String], junavailable:JList[String], root:String,
        report:String, fake:JList[String], immune:JList[String], unknown:Boolean,
        traceOption:Boolean) 
    {
        object ToolSet extends DependencyAnalyser {
            def tracing = traceOption;
            def config = new Configuration {
                val startingClass = root
                val showUnknown = unknown
            }
        }
        import ToolSet._
        
        private def scalaList(i:JIterator[String]) :List[String] =
            if (!i.hasNext()) List()
            else i.next() :: scalaList(i)
        
        private def swcDefinitions(j:JList[String]) :List[Filter] =
            (scalaList(j.iterator()) map (SwcFile(_).catalog.definitions)).toList
        
        private def classFilter(j:JList[String]) :Filter =
            new MatchFilter((scalaList(j.iterator()) map (Class(_))).toList)

        val immunities = FilterTracer(classFilter(immune), "immune")
                
        object AntAnalysis extends Analysis(report) {
            override val categorizer = new Categorizer(
                report.externals, 
                FilterGroup(swcDefinitions(javailable)), 
                FilterGroup(swcDefinitions(junavailable)), 
                FilterTracer(classFilter(fake), "fake"))
                
            override def immune (node:PathNode) :Boolean = immunities.includes(node)
        }
                
        /**
         * Run method called by ant.
         */
        def run () {
            AntAnalysis.run
        }
        
        override def toString() = "an ant analyser"
    }
    
    /**
     * Tool for testing that we can read catalog definitions from a SwcFile
     */
    object DumpDefinitions {
        object ToolSet extends DependencyAnalyser {
            def tracing = false;
            def config = new Configuration {
                val startingClass = "Server"
                val showUnknown = false;
            }
        }
        import ToolSet._
        
        def main(args:Array[String]) = (new SwcFile(args(0))).catalog.definitions.dump
    }   
}